using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using BSDS.Core.Data.Remote.Geocoding.Parsing;
using BSDS.Core.Data.Remote.Geocoding.Serialization;
using BSDS.Core.Infrastructure.Configuration;
using BSDS.Core.Infrastructure.Logging;
using BSDS.Core.Infrastructure.RemoteCommunication;
using BSDS.Core.Model.Data;
using BSDS.Core.Model.Services.Jobs;
using BSDS.Core.Model.Services.Jobs.Core;
using BSDS.Core.Services.BingRestApi;
using BSDS.Core.Services.BingRestApi.Schema;
using System.Collections.Generic;

namespace BSDS.Core.Model.Services.Geocoding
{
    public class GeocodingService : IJobStateProvider
    {
        private const string GeocodingUriTemplate =
            "http://spatial.virtualearth.net/REST/v1/Dataflows/Geocode?description={0}&input=xml&output=xml&key={1}";

        private const string JobStatusUriTemplate =
            "https://spatial.virtualearth.net/REST/v1/dataflows/geocode/{0}?output=xml&key={1}";

        private const string GeocodingResultUriTemplate =
            "https://spatial.virtualearth.net/REST/v1/dataflows/geocode/{0}/output/succeeded?output=xml&key={1}";

        private const string GeocodingFailureUriTemplate =
            "https://spatial.virtualearth.net/REST/v1/dataflows/geocode/{0}/output/failed?key={1}";

        private readonly GeoSerializer geoSerializer;
        private readonly GeoResponseParser geoParser;
        private readonly ServiceClient serviceClient;
        private readonly Logger logger;
        private readonly JobRepository jobRepository;
        private readonly ILocationService locationService;
        private readonly IBingKey bingKey;

        internal GeocodingService(GeoSerializer geoSerializer, GeoResponseParser geoParser, ServiceClient serviceClient, 
            Logger logger, JobRepository jobRepository, ILocationService locationService, IBingKey bingKey)
        {
            this.geoSerializer = geoSerializer;
            this.geoParser = geoParser;
            this.serviceClient = serviceClient;
            this.logger = logger;
            this.jobRepository = jobRepository;
            this.locationService = locationService;
            this.bingKey = bingKey;
        }

        public GeocodeJob ScheduleGeocoding(BsdsData bsdsData, GeocodingMode mode)
        {
            logger.Log("Creating geocoding job");

            if(!bsdsData.GeocodingNeeded(mode))
            {
                throw new ArgumentException("Geocoding is unnecessary. All geocoding data already available.");    
            }

            var dataToGeocode =  bsdsData.GetNotReadyEntities(mode);

            var geoRequestFilename = PrepareGeocodingRequest(mode, dataToGeocode);

            using (var requestStream = new FileStream(geoRequestFilename, FileMode.Open))
            {
                var jobDescription = "Geocoding mode: " + mode + " " + DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");

                var response = serviceClient.SendRequest(string.Format(GeocodingUriTemplate, jobDescription, bingKey), requestStream);

                if(response.Status != HttpStatusCode.Created)
                {
                    logger.Log("Error creating geocoding job.", response);

                    throw new Exception("Error creating geoconding job.");
                }

                var job = CreateJobFromResponse(bsdsData, mode, response);

                return job;
            }
        }

        private GeocodeJob CreateJobFromResponse(BsdsData bsdsData, GeocodingMode geocodingMode, RequestResult response)
        {
            var job = geoParser.ParseCreateGeocodingJobResponse(response.Content);
            job.OriginalFilename = bsdsData.OriginalFilename;
            job.OriginalFormat = bsdsData.OriginalFormat;
            job.Mode = geocodingMode;

            jobRepository.Save(job);

            return job;
        }

        private string PrepareGeocodingRequest(GeocodingMode geocodingMode, EntityValue[] entities)
        {
            string geoRequestFilename;
            
            if(geocodingMode == GeocodingMode.Reverse)
            {
                geoRequestFilename = geoSerializer.SerializeToReverseGeoRequest(entities);
            }
            else
            {
                geoRequestFilename = geoSerializer.SerializeToGeoRequest(entities);
            }
            return geoRequestFilename;
        }
        
        public JobState QueryState(string jobId)
        {
            var response = serviceClient.SendRequest(string.Format(JobStatusUriTemplate, jobId, bingKey), "GET");

            if(response.Status != HttpStatusCode.OK)
            {
                logger.Log("Error getting geocoding job status.", response);
            }

            return geoParser.ParseJobStatusResponse(jobId, response.Content);
        }

        public GeocodingResult DownloadGecodingResults(string jobId)
        {
            logger.Log("Downloading geocoding results for job: " + jobId);

            var response = serviceClient.SendRequest(string.Format(GeocodingResultUriTemplate, jobId, bingKey), "GET");

            if(response.Status != HttpStatusCode.OK)
            {
                logger.Log("Error downloading gecoding results.", response);
            }

            var entities = geoParser.ParseGetGeocodingResultResponse(response.Content);

            return new GeocodingResult(entities);
        }

        public string GetFailureDetials(JobState jobState)
        {
            if(jobState.Status == JobStatus.Aborted)
            {
                return "Job has been aborted.\n" + jobState.AbortReason;
            }
            
            if(jobState.Status == JobStatus.Completed && jobState.EndResult.ComplitionStatus == JobComplitionStatus.Failure)
            {
                return DownloadGeocodingFailureDetails(jobState.Id);
            }

            return string.Empty;
        }

        private string DownloadGeocodingFailureDetails(string jobId)
        {
            logger.Log("Downloading failure information for job: " + jobId);

            var response = serviceClient.SendRequest(string.Format(GeocodingFailureUriTemplate, jobId, bingKey), "GET");

            if (response.Status != HttpStatusCode.OK)
            {
                logger.Log("Error downloading geocoding failure results.", response);
            }

            return response.Content;
        }

        public GeocodingResult GeocodeEntitiesIndividually(EntityValue[] entities)
        {
            var geocodedEntities = new List<EntityValue>();

            foreach (var originalEntity in entities)
            {
                var addressLine = originalEntity.GetAttributeValue("AddressLine");
                var locality = originalEntity.GetAttributeValue("Locality");
                var adminDistrict = originalEntity.GetAttributeValue("AdminDistrict");
                var postalCode = originalEntity.GetAttributeValue("PostalCode");
                var countryRegion = originalEntity.GetAttributeValue("CountryRegion");
                var primaryKey = originalEntity.PrimaryKeyValue;

                if (adminDistrict == "AC" && countryRegion == "AU") //TODO: check other commonly used admin district abbreviations for Australia
                    adminDistrict = "ACT";

                var geocodedEntity = GeocodeAddress(primaryKey, addressLine, locality, adminDistrict, postalCode, countryRegion);

                // Try regeocoding using the postal code instead of the city name if we got back something at the state level
                if (geocodedEntity.GetAttributeValue("MatchedMethod") == "AdminDistrict" && !string.IsNullOrEmpty(postalCode))
                {
                    geocodedEntity = GeocodeAddress(primaryKey, addressLine, "", adminDistrict, postalCode, countryRegion);
                }
// TODO: In Bingmaps.com, "833 Rt 17 South, NJ, 07652" produces the correct result of "833 N State RT 17, Paramus, NJ 07652" but we're 
// not getting the correct result with the call below:
//                else if (geocodedEntity.GetAttributeValue("Confidence") != "High" && !string.IsNullOrEmpty(postalCode))
//                {
//                    geocodedEntity = GeocodeAddress(primaryKey, addressLine, "", adminDistrict, postalCode, countryRegion);
//                }

                geocodedEntities.Add(geocodedEntity);
            }

            return new GeocodingResult(geocodedEntities.ToArray());
        }

        public EntityValue GeocodeAddress(string primaryKey, string addressLine, string locality, string adminDistrict, string postalCode, string countryRegion)
        {
            if (adminDistrict == "AC" && countryRegion == "AU") //TODO: check other commonly used admin district abbreviations for Australia
                adminDistrict = "ACT";

            Location[] locations = new Location[0];
            try
            {
                locations = locationService.FindLocationByAddress(countryRegion, adminDistrict, locality, addressLine, postalCode).ToArray();
            }
            catch
            {
                logger.Log("No response from Locations API for Entity " + primaryKey);
            }

            if (locations.Length == 0)
            {
                return (CreateFailedEntity(primaryKey));
            }

            var newEntity = ParseToEntity(primaryKey, locations[0]);

            // Override the confidence level if address-level match was not achieved (imprecise)
            if (newEntity.GetAttributeValue("MatchedMethod") != "Address")
            {
                newEntity.UpdateAttributeValue("Confidence", "Medium");
            }

            return newEntity;
        }   
        
        private static EntityValue CreateFailedEntity(string EntityID)
        {
            var oelist = new List<AttributeValue>();
            oelist.Add(new AttributeValue(new AttributeInfo("EntityID", "string", true), EntityID));
            oelist.Add(new AttributeValue(new AttributeInfo("Latitude", "string", false), "0"));
            oelist.Add(new AttributeValue(new AttributeInfo("Longitude", "string", false), "0")); 
            oelist.Add(new AttributeValue(new AttributeInfo("Confidence", "string", false), "BadRequest"));
            oelist.Add(new AttributeValue(new AttributeInfo("StatusCode", "string", false), "Failed"));
            return new EntityValue(oelist.ToArray());
        }

        private static EntityValue ParseToEntity(string EntityID, Location location)
        {
            var attributes = new List<AttributeValue>();

            attributes.Add(new AttributeValue(new AttributeInfo("EntityID", "string", true), EntityID));
            AddAttributeIfNotEmpty(attributes, "Latitude", "double", false, location.Point.Latitude.ToString(CultureInfo.InvariantCulture));
            AddAttributeIfNotEmpty(attributes, "Longitude", "double", false, location.Point.Longitude.ToString(CultureInfo.InvariantCulture));
            AddAttributeIfNotEmpty(attributes, "Confidence", "string", false, location.Confidence.ToString());
            AddAttributeIfNotEmpty(attributes, "EntityType", "string", false, location.EntityType);
            AddAttributeIfNotEmpty(attributes, "AddressLine", "string", false, location.Address.AddressLine);
            AddAttributeIfNotEmpty(attributes, "Locality", "string", false, location.Address.Locality);
            AddAttributeIfNotEmpty(attributes, "AdminDistrict", "string", false, location.Address.AdminDistrict);
            AddAttributeIfNotEmpty(attributes, "CountryRegion", "string", false, location.Address.CountryRegion);
            AddAttributeIfNotEmpty(attributes, "FormattedAddress", "string", false, location.Address.FormattedAddress);
            AddAttributeIfNotEmpty(attributes, "PostalCode", "string", false, location.Address.PostalCode);
            AddAttributeIfNotEmpty(attributes, "Geocoder", "string", false, "Locations API");
            AddAttributeIfNotEmpty(attributes, "StatusCode", "string", false, "Success");

            GeoResponseParser.UpdateGeocodePrecision(attributes);
            GeoResponseParser.FixBadRequest(attributes);
            GeoResponseParser.CreateMatchCodeEquivalence(attributes);
            GeoResponseParser.CreateMatchedMethodEquivalence(attributes);

            return new EntityValue(attributes.ToArray());
        }

        private static void AddAttributeIfNotEmpty(List<AttributeValue> attributes, string name, string type, bool isPrimaryKey, string value)
        {
            if (!string.IsNullOrEmpty(value))
            {
                attributes.Add(new AttributeValue(new AttributeInfo(name, type, isPrimaryKey), value));
            }
        }

        private static int ConfidenceToValue(string confidence)
        {
            switch (confidence)
            {
                case "High": return 3;
                case "Medium": return 2;
                case "Low": return 1;
                default: return 0;
            }
        }
    }
}
